﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Net;
using System.Security.Principal;
using Alphaleonis.Win32.Vss;
using Ionic.Zip;
using Ionic.Zlib;

namespace SimpleBackup
{
	class Program
	{
		static void PrintLog(string message) {
			Console.Out.WriteLine("[" + DateTime.Now.ToString(@"yyyy-dd-MM HH:mm:ss") + "] " + message);
		}

		static bool IsUserAdministrator()
		{
			//bool value to hold our return value
			bool isAdmin;
			try
			{
				//get the currently logged in user
				WindowsIdentity user = WindowsIdentity.GetCurrent();
				WindowsPrincipal principal = new WindowsPrincipal(user);
				isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
			}
			catch (UnauthorizedAccessException ex)
			{
				isAdmin = false;
			}
			catch (Exception ex)
			{
				isAdmin = false;
			}
			return isAdmin;
		}
		static void ShowUsage() {
			Console.Error.WriteLine("Backup directory to FTP.\n");
			Console.Error.WriteLine("This program requires elevation!\n");
			Console.Error.WriteLine("Usage: /D:<directory> /F:<ftp> /U:<user> [/P:<password>] [/K:<keep>] [/E:<zip password>]");
			Console.Error.WriteLine("<directory>		- full local name of directory to backup. Volume Shadow Copy used.");
			Console.Error.WriteLine("<ftp>			- name or IP of FTP server.");
			Console.Error.WriteLine("<user>			- FTP user login.");
			Console.Error.WriteLine("<password>		- FTP user password. anonymous@darpa.net by default.");
			Console.Error.WriteLine("<keep>			- keeps last <keep> files based on names natural sorting. 10 by default.");
			Console.Error.WriteLine("<zip password>		- protects ZIP with password provided.");
			Console.Error.WriteLine("\nExample: SimpleBackup.exe /D:\"C:\\Program Files\" /F:ftp.contoso.net /U:login /P:password /K:5");
			Console.Error.WriteLine("By Andriy Lishchytovych, 2014-05-13.");
		}

		static void CreateZip(string dir, string zipName, string zipPwd) {
			using(ZipFile zip = new ZipFile()) {
				if(zipPwd!="")
					zip.Password = zipPwd;
				zip.AlternateEncoding = Encoding.UTF8;
				zip.AlternateEncodingUsage = ZipOption.AsNecessary;
				zip.UseZip64WhenSaving = Zip64Option.AsNecessary;
				zip.Encryption = EncryptionAlgorithm.WinZipAes256;
				zip.AddDirectory(dir);
				zip.Save(zipName);
			}
		}

		static void UploadFTP(string fname, string ftp, string user, string password) {
			string fn = new FileInfo(fname).Name;
			Uri Output = new Uri("ftp://" + ftp + "/" + fn);
			FtpWebRequest request = (FtpWebRequest)WebRequest.Create(Output);
			request.Method = WebRequestMethods.Ftp.UploadFile;

			request.UsePassive = true;
			request.Credentials = new NetworkCredential(user, password);

			// Copy the contents of the file to the request stream.
			Stream dest = request.GetRequestStream();
			FileStream src = File.OpenRead(fname);

			int bufSize = (int)Math.Min(src.Length, 1024);
			byte[] buffer = new byte[bufSize];
			int bytesRead = 0;

			int numBuffersUploaded = 0;

			do {
				bytesRead = src.Read(buffer, 0, bufSize);
				dest.Write(buffer, 0, bufSize);
				numBuffersUploaded++;
			}
			while(bytesRead != 0);

			dest.Close();
			src.Close();

			FtpWebResponse response = (FtpWebResponse)request.GetResponse();
			if(response.StatusCode != FtpStatusCode.ClosingData) {
				Console.Error.WriteLine("Error uploading file to FTP server: {0} ({1})", response.StatusDescription, response.StatusCode);
			} 
		}

		static void ClearFTP(string ftp, string user, string password, int keep) {
			NetworkCredential login = new NetworkCredential(user, password);
			FtpWebRequest ftpRequest = (FtpWebRequest)WebRequest.Create("ftp://"+ftp);
			ftpRequest.Credentials = login;
			ftpRequest.Method = WebRequestMethods.Ftp.ListDirectoryDetails;
			FtpWebResponse response = (FtpWebResponse)ftpRequest.GetResponse();
			StreamReader streamReader = new StreamReader(response.GetResponseStream());

			List<string> files = new List<string>();

			string line = streamReader.ReadLine();
			while (!string.IsNullOrEmpty(line) && line.Substring(0,1)!="d")
			{
				files.Add(line.Substring(62));
				line = streamReader.ReadLine();
			}
			streamReader.Close();

			files.Sort((x, y) => {
				int ix, iy;
				return int.TryParse(x, out ix) && int.TryParse(y, out iy) ? ix.CompareTo(iy) : string.Compare(x, y);
			});

			// Optimization possible not to reconnect on every delete
			for(int i = 0; i < files.Count() - keep; i++) { 
				ftpRequest = (FtpWebRequest)WebRequest.Create("ftp://" + ftp + "/"+ files[i]);
				ftpRequest.UsePassive = true;
				ftpRequest.Credentials = login;
				ftpRequest.Method = WebRequestMethods.Ftp.DeleteFile;
				response = (FtpWebResponse)ftpRequest.GetResponse();
			}
		}
		static int Main(string[] args)
		{
			string directory = "", ftp = "", user = "", password = "anonymous@darpa.net", zipPwd="";
			int keep = 10; // Keeps 10 files by default
			int ret = 0;

			if (!IsUserAdministrator()) {
				ShowUsage();
				return 1;
			}

			try {
				char[] trim = {' ','"'};
				for(int i = 0; i < args.Length; i++) {
					switch(args[i].Substring(0, 3).ToUpper()) {
						case "/D:":
							directory = args[i].Substring(3).Trim(trim);
							break;
						case "/F:":
							ftp = args[i].Substring(3);
							break;
						case "/U:":
							user = args[i].Substring(3).Trim(trim);
							break;
						case "/P:":
							password = args[i].Substring(3);
							break;
						case "/K:":
							keep = int.Parse(args[i].Substring(3));
							break;
						case "/E:":
							zipPwd = args[i].Substring(3);
							break;
						default:
							Console.Error.WriteLine("Unknown parameter: " + args[i] + "\n");
							ShowUsage();
							return 2;
					}
				}
				if(directory == "" || ftp == "" || user == "") {
					ShowUsage();
					return 2;
				}
			}
			catch(Exception ex) {
				Console.Error.WriteLine("Incorrect parameters");
				return 2;
			}
			PrintLog("VSS creation started...");
			string shadowDrive = "B:";
			string zipFN = DateTime.Now.ToString(@"yyyy-dd-MM-HH-mm-ss") + "_backup.zip";			
			string zipName = System.IO.Path.GetTempPath() + zipFN;
			FileInfo sourceInfo = new FileInfo(directory);
			String volume = sourceInfo.Directory.Root.Name;

			IVssImplementation vssImplementation = VssUtils.LoadImplementation();
			IVssBackupComponents backup = vssImplementation.CreateVssBackupComponents();
			
			backup.InitializeForBackup(null);

			backup.GatherWriterMetadata();

			// VSS step 3: VSS Configuration

			//backup.SetContext(VssVolumeSnapshotAttributes.NoAutoRecovery | VssVolumeSnapshotAttributes.NoAutoRelease);
			//backup.SetContext(VssVolumeSnapshotAttributes.Persistent | VssVolumeSnapshotAttributes.NoAutoRecovery);
			backup.SetContext(VssVolumeSnapshotAttributes.Persistent | VssVolumeSnapshotAttributes.NoAutoRelease);
			
			backup.SetBackupState(false, true, Alphaleonis.Win32.Vss.VssBackupType.Full, false);

			Guid MyGuid01 = backup.StartSnapshotSet();
			Guid MyGuid02 = backup.AddToSnapshotSet(volume, Guid.Empty);


			backup.PrepareForBackup();

			backup.DoSnapshotSet();
			backup.ExposeSnapshot(MyGuid02, null, VssVolumeSnapshotAttributes.ExposedLocally, shadowDrive);

			PrintLog("VSS created.");
			String VSSDir = directory.Replace(volume, shadowDrive + @"\");
			try {
				if(Directory.Exists(VSSDir)) {
					PrintLog("Archive creation started for " + directory+"...");
					CreateZip(VSSDir, zipName, zipPwd);
					PrintLog("Archive " + zipFN + " created.");
					ClearFTP(ftp, user, password, keep);
					PrintLog("FTP upload started...");
					UploadFTP(zipName, ftp, user, password);
					PrintLog("FTP upload completed.");
				}
			}
			catch(Exception ex) {
				Console.Error.WriteLine("Failure!\nException: " + ex.Message);
				ret = 3;
			}
			finally {
				foreach(VssSnapshotProperties prop in backup.QuerySnapshots()) {
					if(prop.ExposedName == shadowDrive + @"\") {
						backup.DeleteSnapshot(prop.SnapshotId, true);
						PrintLog("VSS deleted.");
					}
				}
				File.Delete(zipName);
			}
			backup = null;
			return ret;
		}
	}
}
